package dht.threads;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.PipedInputStream;
import java.io.PipedOutputStream;
import java.math.BigInteger;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.SocketException;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.zip.GZIPInputStream;

import dht.communication.CommunicationDHT;
import dht.communication.FabriqueMessage;
import dht.communication.PortThread;
import dht.data.Contexte;
import dht.data.DhtData;
import dht.data.TabBigInteger;
import dht.interfaceThread.InterJoin;
import dht.message.ContexteMessage;
import dht.message.JoinMessage;
import dht.message.MajMessage;
import dht.message.Message;
import dht.message.exception.ErrorMessage;
import dht.node.Node;
import dht.tools.Couple;

/**
 * Thread qui gere la reception et l'envoie de message JOIN.
 * @author bouceka
 *
 */
public class ManageJoin extends Thread implements InterJoin {

	private DatagramSocket maSocket = null;
	private DatagramSocket socketReceiveJOIN = null;
	private boolean initNode;
	private Message msgRecu = null;

	public boolean initFinished = false;

	public ManageJoin(boolean initNode){
		this.initNode = initNode;

		try {
			this.socketReceiveJOIN = new DatagramSocket(PortThread.PORT_JOIN);
		} catch (SocketException e) {
			e.printStackTrace();
		} 
	}
	
	/**
	 * Fonction qui envoit un message de mise a jour a tous les
	 * voisins pour qu'ils reconnaissent le noeud courant.
	 * @param addrAncienVoisin l'adresse de l'ancien voisin si il doit etre remplacé
	 * dans la liste des voisins du destinataire.
	 * @throws UnknownHostException
	 */
	private void majVoisins(String addrAncienVoisin) throws UnknownHostException{

		MajMessage mm = null;
		int dim = 0;

		synchronized(Node.myContext){

			for(ArrayList<Couple<String>> liste : Node.myContext.getVoisinage()){
				for(Couple<String> voisin : liste){
					if(addrAncienVoisin.compareTo(voisin.getElem2()) != 0){
						
						dim = this.dimVoisin(voisin.getElem2());
						
						if(dim < 0) 
							System.out.println("Thread ManageJoin erreur dim ancien voisin !");
						
						mm = FabriqueMessage.createMajMessage(Node.myContext.getAdresseIP(), Node.myContext.getVirtualID(), 
								                              dim, addrAncienVoisin);
						
						InetAddress dest = InetAddress.getByName(voisin.getElem2());
						DatagramPacket p = CommunicationDHT.packetForgeToKnownNode(mm, dest, PortThread.PORT_RECEIVE_MAJ);
						CommunicationDHT.sendMessage(p, maSocket);
					}
				}
			}
		}
		return;
	}

	/**
	 * Fonction qui retourne la dimension auquel appartient
	 * un voisin.
	 * @param ancienVoisin 
	 * l'adresse du voisin
	 * @return la dimension
	 */
	private int dimVoisin(String ancienVoisin){

		synchronized(Node.myContext){
			System.out.println(ancienVoisin);
			int sizeI = 0, sizeJ = 0;

			sizeI = Node.myContext.getVoisinage().size();
			for(int i=0; i<sizeI; i++){
				sizeJ = Node.myContext.getVoisinage().get(i).size();
				for(int j=0; j<sizeJ ;j++){
					if(Node.myContext.getVoisinage().get(i).get(j).getElem2().compareTo(ancienVoisin) == 0){
						System.out.println(Node.myContext.getVoisinage().get(i).get(j).getElem2());

						return i;
					}

				}
			}
		}

		return -1;
	}

	//Receptionne le contexte puis initialise le noeud.
	private void receiveContextAndInitNode() throws ErrorMessage, UnknownHostException {

		ServerSocket ss = null; // On receptionne le Contexte avec une connexion TCP
		Socket sc = null;
		Message result = null;
		ObjectInputStream ois = null;
		Contexte ctx = null;

		try {
			ss = new ServerSocket(PortThread.PORT_RECEIVE_CTX);
			sc = ss.accept();
		} catch (IOException e) {
			e.printStackTrace();
		}

		try {
			ois = new ObjectInputStream(new GZIPInputStream(sc.getInputStream()));
			result = (Message)ois.readObject();
			ois.close();

		} catch (ClassNotFoundException e) {
			e.printStackTrace();
		} catch (IOException e) {
			e.printStackTrace();
		}

		// On peut initialiser le noeud a partir du contexte recu
		if(result.TYPE_MSG == Message.CONTEXTE_Message){
			ctx = ((ContexteMessage)result).getContexte();

			synchronized(Node.myContext){
				Node.myContext.setZoneLocale(ctx.getZoneLocale());
				Node.myContext.setDhtLocale(ctx.getDhtLocale());
				Node.myContext.setVoisinage(ctx.getVoisinage());
				Node.myContext.setVirtualID(ctx.getVirtualID());
			}
		}
		else{
			throw new ErrorMessage();
		}

		String ancienVoisin = result.getadresseIpExp().toString().substring(1);

		System.out.println("Ancien Voisin :" + ancienVoisin);

		this.majVoisins(ancienVoisin);

		System.out.println("Contexte recu : \n" + Node.myContext);
		initFinished = true;
		return;
	}

	/**
	 * Indique si le point appartient a la zone courante
	 * Fonction appelé dans un contexte possedant le verrou sur Node.myContexte
	 * @param point Le point a testé
	 * @return retourne vrai si le point appartient a la zone, faux sinon.
	 */
	private boolean isInzone(TabBigInteger point){

		for(int i = 0; i<Node.NB_DIMENSION ; i++){
			// plus petit que le min
			if(point.getValue(i).compareTo(Node.myContext.getZoneLocale().get(i).getElem1()) < 0){
				return false;
			}

			// plus grand que le max
			if(point.getValue(i).compareTo(Node.myContext.getZoneLocale().get(i).getElem2()) > 0){
				return false;
			}
		}
		return true;
	}
	
	/**
	 * Fonction qui cree le contexte du noeud entrant
	 * a partir du contexte courant.
	 * @return
	 */
	@SuppressWarnings("unchecked")
	private ContexteMessage createContexte(){

		Couple<String> mySelf = null;
		InetAddress myAddrrIP = null;
		ArrayList<Couple<BigInteger>> zoneLocalDest = null;
		ArrayList<ArrayList<Couple<String>>> voisinageDest = null;
		HashMap<TabBigInteger, DhtData<Integer>> dhtLocalDest = null;
		Couple<String> newVoisin = null;
		String virtualIdDest = null;
		Contexte ctx = null;

		BigInteger min = null, max = null;

		synchronized(Node.myContext){

			// On utilise des pipes pour faire des copies profonde par serialisation
			PipedInputStream pis = new PipedInputStream();
			PipedOutputStream pos = null;
			ObjectOutputStream os = null;
			ObjectInputStream is = null;

			try {
				pos = new PipedOutputStream(pis);
			} catch (IOException e) {
				e.printStackTrace();
			}

			// 1. Copie profonde de la zone local du noeud distant
			try {
				os = new ObjectOutputStream(pos);
				is = new ObjectInputStream(pis);
				os.writeObject(Node.myContext.getZoneLocale());
			} catch (IOException e) {
				e.printStackTrace();
			}

			try {
				zoneLocalDest = (ArrayList<Couple<BigInteger>>) is.readObject();
			} catch (IOException e1) {
				e1.printStackTrace();
			} catch (ClassNotFoundException e1) {
				e1.printStackTrace();
			}

			// 2. Copie profonde du voisinage courant

			try {
				os.writeObject(Node.myContext.getVoisinage());
			} catch (IOException e) {
				e.printStackTrace();
			}

			try {
				voisinageDest = (ArrayList<ArrayList<Couple<String>>>)is.readObject();
				is.close();
				os.close();
			} catch (IOException e1) {
				e1.printStackTrace();
			} catch (ClassNotFoundException e1) {
				e1.printStackTrace();
			}

			min = Node.myContext.getZoneLocale().get(Node.dimensionCut).getElem1();
			max = Node.myContext.getZoneLocale().get(Node.dimensionCut).getElem2();

			BigInteger add = max.add(min);
			BigInteger milieu = add.divide(new BigInteger("2",10));
			BigInteger milieuPlusUn = milieu.add(new BigInteger("1",10));

			// modification du max de la zone du destinataire
			zoneLocalDest.get(Node.dimensionCut).setElem1(milieuPlusUn); 

			// modification du min de la zone locale
			Node.myContext.getZoneLocale().get(Node.dimensionCut).setElem2(milieu);

			// 3. On cree la table de hachage du noeud distant
			dhtLocalDest = new HashMap<TabBigInteger, DhtData<Integer>>();

			for(TabBigInteger elem : Node.myContext.getDhtLocale().keySet()){
				if(!this.isInzone(elem)){
					dhtLocalDest.put(elem, Node.myContext.getDhtLocale().get(elem));
					Node.myContext.getDhtLocale().remove(elem);
				}
			}

			// 4. On cree le virtual id du noeud entrant
			virtualIdDest = Node.myContext.getVirtualID() + Node.dimensionCut + "1:";

			//Mise a jour du vid du noeud courant
			Node.myContext.setVirtualID(Node.myContext.getVirtualID() + Node.dimensionCut + "0:");

			// Le noeud entrant
			newVoisin = new Couple(virtualIdDest,this.msgRecu.getadresseIpExp().getHostAddress());

			// On rajoute le noeud entrant au voisinage courant
			if(Node.myContext.getVoisinage().get((Node.dimensionCut*2)+1) == null)
				Node.myContext.getVoisinage().set((Node.dimensionCut*2)+1,new ArrayList<Couple<String>>());
			Node.myContext.getVoisinage().get((Node.dimensionCut*2)+1).add(newVoisin);

			try {
				myAddrrIP = InetAddress.getByName(CommunicationDHT.getLocalIp());
			} catch (UnknownHostException e) {
				e.printStackTrace();
			}

			// Mise a jour des voisins du noeud entrant
			mySelf = new Couple(Node.myContext.getVirtualID(),myAddrrIP.getHostAddress());
			
			voisinageDest.set(Node.dimensionCut*2,new ArrayList<Couple<String>>());
			voisinageDest.get(Node.dimensionCut*2).add(mySelf);
			

			// Mise a jour de la dimension a couper
			Node.dimensionCut = (Node.dimensionCut + 1) % Node.NB_DIMENSION;
		}

		ctx = new Contexte(zoneLocalDest, dhtLocalDest, voisinageDest, virtualIdDest, this.msgRecu.getadresseIpExp());

		return new ContexteMessage(myAddrrIP, ctx);
	}

	/**
	 * Fonction de traitement d'une requete JOIN.
	 */
	public void receiveRequestJoin() throws ErrorMessage {

		DatagramPacket in = null;
		byte[] buff = null;
		ByteArrayInputStream monStream = null;
		ObjectInputStream ois = null;

		buff = new byte[65536];
		in  = new DatagramPacket(buff,65536);

		System.out.println("Thread ManageJoin : Attente requete JOIN...");

		try {
			this.socketReceiveJOIN.receive(in); // Attente d'une requete
		} catch (IOException e) {
			e.printStackTrace();
		}

		monStream = new ByteArrayInputStream(buff);

		try {
			ois = new ObjectInputStream(monStream);
			msgRecu = (Message)ois.readObject();
		} catch (IOException e) {
			e.printStackTrace();
		} catch (ClassNotFoundException e) {
			e.printStackTrace();
		}

		if(this.msgRecu.TYPE_MSG == Message.JOIN_Message){

			// Le noeud courant est le destinataire donc on envoie un contexte au noeud distant.
			if(this.isInzone(this.msgRecu.getDestination())){

				System.out.println("Recu requete JOIN");
				ContexteMessage ctxDestinataire = this.createContexte();

				CommunicationDHT.sendContexte(ctxDestinataire, this.msgRecu.getadresseIpExp(),PortThread.PORT_RECEIVE_CTX);

				System.out.println("Le contexte a ete envoye au noeud entrant !");
				System.out.println("Mon contexte : \n" + Node.myContext.toString());
			}
			else{ // Le noeud courant n'est pas le destinataire, on doit router le message vers le noeud le plus proche
				DatagramPacket p = CommunicationDHT.packetForgeToClosestOrRandomNode(this.msgRecu, PortThread.PORT_JOIN);
				CommunicationDHT.sendMessage(p, maSocket);
			}
		}
		else{
			throw new ErrorMessage();
		}

		return;
	}

	/**
	 * sendRequestJoin envoie un JoinMessage pour rejoindre le reseau puis
	 * receptionne un contexte pour s'initialiser.
	 * @param msg
	 * @return true si opération réussi, false sinon.
	 */
	public boolean sendRequestJoin(JoinMessage msg) {

		InetAddress adresseIpClient = null;
		InetAddress noeudConnu = null;
		JoinMessage joinMsg = null; 
		DatagramPacket myData = null;

		if(msg == null){ // Si la fonction est appelé pour l'initialisation du noeud.

			try {
				adresseIpClient = InetAddress.getByName(CommunicationDHT.getLocalIp());
				noeudConnu = InetAddress.getByName(Node.adresseIpConnu); // ** NOEUD CONNU, doit etre en dur, ou passe en parametre
			} catch (UnknownHostException e) {
				e.printStackTrace();
			}

			joinMsg = FabriqueMessage.createJoinMessage(adresseIpClient);
			myData = CommunicationDHT.packetForgeToKnownNode(joinMsg, noeudConnu, PortThread.PORT_JOIN);

			CommunicationDHT.sendMessage(myData,this.maSocket);

			try {
				this.receiveContextAndInitNode();
			} catch (ErrorMessage e) {
				e.printStackTrace();
			} catch (UnknownHostException e) {
				e.printStackTrace();
			}
		}
		else{ // Sinon la fonction est appelé dans le cadre du routage, il faut donc router le message
			// vers le noeud le plus proche de la destination.
			DatagramPacket p = CommunicationDHT.packetForgeToClosestOrRandomNode(msg, PortThread.PORT_JOIN);
			CommunicationDHT.sendMessage(p, maSocket);
		}

		return true;
	}

	public void run() {

		try {
			this.maSocket = new DatagramSocket();
		} catch (SocketException e) {
			e.printStackTrace();
		}

		if(!this.initNode){ // Si ce n'est pas un noeud principal, alors envoyer une requete JOIN pour l'initialiser
			System.out.println("Envoie requete JOIN");
			this.sendRequestJoin(null);
		}

		System.out.println("Noeud initialise !");

		while(true){ // Boucle de reception des requetes JOIN.

			try {
				this.receiveRequestJoin();
			} catch (ErrorMessage e) {
				e.printStackTrace();
			}
		}
	}
}
